package top.codedance.iotp.client.plugins.sync.modbus;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import top.codedance.iotp.client.ClientApplication;
import top.codedance.iotp.client.net.listenner.PluginService;
import top.codedance.iotp.common.constent.SlaveDefinition;
import top.codedance.iotp.common.entity.PluginResultEntity;
import com.fazecast.jSerialComm.SerialPort;
import com.serotonin.modbus4j.BatchRead;
import com.serotonin.modbus4j.BatchResults;
import com.serotonin.modbus4j.ModbusFactory;
import com.serotonin.modbus4j.ModbusMaster;
import com.serotonin.modbus4j.locator.BaseLocator;
import com.serotonin.modbus4j.msg.*;
import org.apache.log4j.Logger;
import org.ini4j.Ini;
import org.ini4j.Wini;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

public class ModbusASCIIService implements PluginService, Runnable {
    private Logger logger = ClientApplication.getLogger(ModbusASCIIService.class);

    private static final String SERVICE_NAME = "ModbusASCIIService";
    private Integer clientId = 0;
    private final ModbusFactory factory = new ModbusFactory();
    private ModbusMaster master = null;
    private volatile SerialPortWrapperImpl serialPortWrapper = null;
    private volatile CountDownLatch cdl = new CountDownLatch(1);
    private Wini ini = null;

    public ModbusASCIIService(Integer clientId, Wini ini) {
        this.clientId = clientId;
        this.ini = ini;
        factory.setLoggerLevel(logger.getLevel().toString());
    }

    public static PluginService getInstance(Integer clientId, Wini ini) {
        PluginService pluginService = ModbusExecuterImpl.getPluginService(SERVICE_NAME + clientId);
        if (null == pluginService) {
            ModbusASCIIService modbusASCIIService = new ModbusASCIIService(clientId, ini);
            ModbusExecuterImpl.pluginServiceWorkerPool.submit(modbusASCIIService);
            pluginService = ModbusExecuterImpl.registPluginService(SERVICE_NAME + clientId, modbusASCIIService);
        }
        return pluginService;
    }

    @Override
    public void run() {
        ModbusExecuterImpl.pluginServiceWorkerPool.submit(() -> {
            connectChecker();
        });
    }

    /**
     * 连接检测器
     */
    private void connectChecker(){
        try {
            while (true) {
                synchronized (this.getClass()) {
                    logger.debug("Checker modbusASCII connection.");
                    if (master == null || !master.isConnected() || serialPortWrapper == null || !serialPortWrapper.isOpen()) {
                        try {
                            cdl = new CountDownLatch(1);
                            destory();
                            Thread.sleep(1000);
                            init();
                            logger.debug("Connect MobusASCII client.");
                        } catch (Exception e){
                            logger.debug(e.getMessage());
                        }
                    }
                    try {
                        Thread.sleep(1000);
                    }catch (Exception e){
                        logger.debug(e.getMessage());
                    }
                }
            }
        }catch (Exception e){
            logger.debug(e.getMessage());
        }finally {
            connectChecker();
        }
    }

    @Override
    public void init() {
        createConn(clientId);
    }

    /**
     * 断开连接
     */
    private synchronized void destory() {
        if (serialPortWrapper != null && !serialPortWrapper.isOpen()) {
            try {
                serialPortWrapper.close();
                serialPortWrapper = null;
            } catch (Exception e) {
            }
        }
        if (master != null) {
            master.setConnected(false);
            try {
                master.destroy();
            } catch (Exception e) {
            }
            master = null;
        }
        logger.debug("Over MobusASCII client.");
    }

    /**
     * 创建连接
     * @param clientId
     */
    private synchronized void createConn(Integer clientId) {
        try {
            int baudRate = 9600, dataBits = 8, stopBits = SerialPort.ONE_STOP_BIT, parity = SerialPort.NO_PARITY, comClientId = 0;
            String comName = null;
            if (ini != null && ini.containsKey("modbus")) {
                Ini.Section section = ini.get("modbus");
                if (section.containsKey("com." + clientId)) {
                    comClientId = Integer.valueOf(section.get("com." + clientId));
                    Ini.Section sectionCom = ini.get("com");
                    if (sectionCom.containsKey("comName." + comClientId)) {
                        comName = sectionCom.get("comName." + comClientId);
                    }
                    if (sectionCom.containsKey("baudRate." + comClientId)) {
                        baudRate = Integer.parseInt(sectionCom.get("baudRate." + comClientId));
                    }
                    if (sectionCom.containsKey("dataBits." + comClientId)) {
                        dataBits = Integer.parseInt(sectionCom.get("dataBits." + comClientId));
                    }
                    if (sectionCom.containsKey("stopBits." + comClientId)) {
                        stopBits = Integer.parseInt(sectionCom.get("stopBits." + comClientId));
                    }
                    if (sectionCom.containsKey("parity." + comClientId)) {
                        parity = Integer.parseInt(sectionCom.get("parity." + comClientId));
                    }
                }
            }
            if(serialPortWrapper == null || !serialPortWrapper.isOpen()) {
                serialPortWrapper = new SerialPortWrapperImpl(comName, baudRate, dataBits, stopBits, parity);
            }
            master = factory.createAsciiMaster(serialPortWrapper);
            master.init();
            if(!serialPortWrapper.isOpen()) {
                master.destroy();
                serialPortWrapper.close();
                Thread.sleep(3000);
                throw new Exception("Com port connect fail.");
            }
            master.setConnected(true);
            logger.debug("ModbusASCII conneted.");
        } catch (Exception e) {
            logger.debug(e.getMessage());
        } finally {
            cdl.countDown();
        }
    }

    private void waiting(){
        try {
            cdl.await(3, TimeUnit.SECONDS);
        }catch (Exception e){}
    }

    @Override
    public PluginResultEntity execute(JSONObject jo){
        waiting();
        PluginResultEntity endResult = new PluginResultEntity();
        if (master != null && master.isConnected()) {
            JSONArray outResult = new JSONArray();
            if(jo.containsKey("devices")) {
                int startSlaveId = jo.containsKey("first") ? jo.getIntValue("first") : 1;
                for (int i = startSlaveId;i <= jo.getIntValue("devices");i++) {
                    try {
                        ModbusRequest req = new ReadHoldingRegistersRequest(i, 0, 2);
                        ModbusResponse response = master.send(req);
                        if (response instanceof ReadHoldingRegistersResponse) {
                            JSONObject tjo = new JSONObject();
                            tjo.put("slaveId", i);
                            tjo.put("onLine", true);
                            outResult.add(tjo);
                        }
                    } catch (Exception e) {
                        JSONObject tjo = new JSONObject();
                        tjo.put("slaveId", i);
                        tjo.put("onLine", false);
                        outResult.add(tjo);
                        logger.debug(e.getMessage());
                    }
                }
            }
            endResult.setContext(outResult);
            endResult.setOk(true);
        } else {
            endResult.setOk(false);
            endResult.setMsg("MobusASCII client is disconnected.");
        }
        return endResult;
    }

    @Override
    public PluginResultEntity execute(JSONArray items) {
        waiting();
        PluginResultEntity results = new PluginResultEntity();
        if (master != null && master.isConnected()) {
            JSONObject data = new JSONObject();
            JSONArray readItems = new JSONArray();
            JSONArray writeItems = new JSONArray();
            Iterator<JSONObject> iterator = items.iterator();
            JSONObject item;
            while (iterator.hasNext()) {
                item = iterator.next();
                if (item.containsKey("isWrite") && item.getBoolean("isWrite")) {
                    writeItems.add(item);
                } else {
                    readItems.add(item);
                }
            }
            if(readItems.size()>0) {
                batchRead(readItems, data);
            }
            if(writeItems.size()>0){
                batchWrite(writeItems, data);
            }
            results.setOk(true);
            results.setContext(data);
        } else {
            results.setOk(false);
            results.setMsg("MobusASCII client is disconnected.");
        }
        return results;
    }

    private void batchRead(JSONArray items, JSONObject results){
        Iterator<JSONObject> iterator = items.iterator();
        ArrayList<String> tags = new ArrayList<>();
        JSONObject item;
        BatchRead<String> br = new BatchRead();
        while (iterator.hasNext()) {
            item = iterator.next();
            if (item.containsKey("function") && item.containsKey("slaveId") && item.containsKey("offset") && item.containsKey("datatype") && item.containsKey("tag")) {
                BaseLocator<Boolean> booleanBaseLocator;
                BaseLocator<Number> numberBaseLocator;
                if (item.getIntValue("function") == SlaveDefinition.COIL_STATUS.getValue()) {
                    booleanBaseLocator = BaseLocator.coilStatus(item.getIntValue("slaveId"), item.getIntValue("offset"));
                    br.addLocator(item.getString("tag"), booleanBaseLocator);
                }
                if (item.getIntValue("function") == SlaveDefinition.INPUT_STATUS.getValue()) {
                    booleanBaseLocator = BaseLocator.inputStatus(item.getIntValue("slaveId"), item.getIntValue("offset"));
                    br.addLocator(item.getString("tag"), booleanBaseLocator);
                }
                if (item.getIntValue("function") == SlaveDefinition.HOLDING_REGISTERS.getValue()) {
                    numberBaseLocator = BaseLocator.holdingRegister(item.getIntValue("slaveId"), item.getIntValue("offset"), item.getIntValue("datatype"));
                    br.addLocator(item.getString("tag"), numberBaseLocator);
                }
                if (item.getIntValue("function") == SlaveDefinition.INPUT_REGISTERS.getValue()) {
                    numberBaseLocator = BaseLocator.inputRegister(item.getIntValue("slaveId"), item.getIntValue("offset"), item.getIntValue("datatype"));
                    br.addLocator(item.getString("tag"), numberBaseLocator);
                }
                tags.add(item.getString("tag"));
            }
        }
        try {
            br.setContiguousRequests(true);
            BatchResults<String> res = master.send(br);
            for (String tag : tags) {
                results.put(tag, res.getValue(tag));
            }
        }catch (Exception ex){
            logger.debug(ex.getMessage());
            logger.debug("MobusASCII has exception now restart!");
        }
    }

    private void batchWrite(JSONArray items, JSONObject results){
        Iterator<JSONObject> iterator = items.iterator();
        try {
            while (iterator.hasNext()) {
                write(iterator.next(), results);
            }
        }catch (Exception ex){
            logger.debug(ex.getMessage());
            logger.debug("MobusASCII has exception now restart!");
        }
    }

    private void write(JSONObject item, JSONObject results) throws Exception {
        if (item.containsKey("value") && item.containsKey("function") && item.containsKey("slaveId") && item.containsKey("offset") && item.containsKey("datatype") && item.containsKey("tag")) {
            try {
                if (item.getIntValue("function") == SlaveDefinition.COIL_STATUS.getValue()) {
                    WriteCoilRequest writeCoilRequest = new WriteCoilRequest(item.getIntValue("slaveId"), item.getIntValue("offset"), item.getBoolean("value"));
                    ModbusResponse response = master.send(writeCoilRequest);
                    results.put(item.getString("tag"), !response.isException() ? item.getBoolean("value") : null);
                }
                if (item.getIntValue("function") == SlaveDefinition.INPUT_STATUS.getValue()) {
                    BaseLocator<Boolean> booleanBaseLocator = BaseLocator.inputStatus(item.getIntValue("slaveId"), item.getIntValue("offset"));
                    master.setValue(booleanBaseLocator, item.getBoolean("value"));
                    results.put(item.getString("tag"), item.getBoolean("value"));
                }
                if (item.getIntValue("function") == SlaveDefinition.HOLDING_REGISTERS.getValue()) {
                    BaseLocator<Number> numberBaseLocator = BaseLocator.holdingRegister(item.getIntValue("slaveId"), item.getIntValue("offset"), item.getIntValue("datatype"));
                    master.setValue(numberBaseLocator, item.get("value"));
                    results.put(item.getString("tag"), item.get("value"));
                }
                if (item.getIntValue("function") == SlaveDefinition.INPUT_REGISTERS.getValue()) {
                    BaseLocator<Number> numberBaseLocator = BaseLocator.inputRegister(item.getIntValue("slaveId"), item.getIntValue("offset"), item.getIntValue("datatype"));
                    master.setValue(numberBaseLocator, item.get("value"));
                    results.put(item.getString("tag"), item.get("value"));
                }
                if (item.getIntValue("function") == SlaveDefinition.WRITE_REGISTER.getValue()) {
                    BaseLocator<Number> numberBaseLocator = BaseLocator.writeRegister(item.getIntValue("slaveId"), item.getIntValue("offset"), item.getIntValue("datatype"));
                    master.setValue(numberBaseLocator, item.get("value"));
                    results.put(item.getString("tag"), item.get("value"));
                }
                if (item.getIntValue("function") == SlaveDefinition.WRITE_REGISTERS.getValue()) {
                    BaseLocator<Number> numberBaseLocator = BaseLocator.writeRegisters(item.getIntValue("slaveId"), item.getIntValue("offset"), item.getIntValue("datatype"));
                    master.setValue(numberBaseLocator, item.get("value"));
                    results.put(item.getString("tag"), item.get("value"));
                }
            } catch (Exception e) {
                results.put(item.getString("tag"), null);
                throw e;
            }
        } else {
            results.put(item.getString("tag"), false);
        }
    }
}
